<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_17</span></h2>

  <p>己对象的类型，那么instanceof的使用是非常重要的――否则会得到一个ClassCastException违例。</p>

  <p>我们最一般的做法是查找一种类型（比如要变成紫色的三角形），但下面这个程序却演示了如何用instanceof标记出所有对象。</p>

  <p>//: PetCount.java</p>

  <p>// Using instanceof</p>

  <p>package c11.petcount;</p>

  <p>import java.util.*;</p>

  <p>class Pet {}</p>

  <p>class Dog extends Pet {}</p>

  <p>class Pug extends Dog {}</p>

  <p>class Cat extends Pet {}</p>

  <p>class Rodent extends Pet {}</p>

  <p>class Gerbil extends Rodent {}</p>

  <p>class Hamster extends Rodent {}</p>

  <p>class Counter { int i; }</p>

  <p>public class PetCount {</p>

  <p>static String[] typenames = {</p>

  <p>"Pet", "Dog", "Pug", "Cat",</p>

  <p>"Rodent", "Gerbil", "Hamster",</p>

  <p>};</p>

  <p>public static void main(String[] args) {</p>

  <p>Vector pets = new Vector();</p>

  <p>try {</p>

  <p>Class[] petTypes = {</p>

  <p>Class.forName("c11.petcount.Dog"),</p>

  <p>Class.forName("c11.petcount.Pug"),</p>

  <p>Class.forName("c11.petcount.Cat"),</p>

  <p>Class.forName("c11.petcount.Rodent"),</p>

  <p>Class.forName("c11.petcount.Gerbil"),</p>

  <p>Class.forName("c11.petcount.Hamster"),</p>

  <p>};</p>

  <p>for(int i = 0; i &lt; 15; i++)</p>

  <p>pets.addElement(</p>

  <p>petTypes[</p>

  <p>(int)(Math.random()*petTypes.length)]</p>

  <p>.newInstance());</p>

  <p>} catch(InstantiationException e) {}</p>

  <p>catch(IllegalAccessException e) {}</p>

  <p>catch(ClassNotFoundException e) {}</p>

  <p>Hashtable h = new Hashtable();</p>

  <p>for(int i = 0; i &lt; typenames.length; i++)</p>

  <p>h.put(typenames[i], new Counter());</p>

  <p>for(int i = 0; i &lt; pets.size(); i++) {</p>

  <p>Object o = pets.elementAt(i);</p>

  <p>if(o instanceof Pet)</p>

  <p>((Counter)h.get("Pet")).i++;</p>

  <p>if(o instanceof Dog)</p>

  <p>((Counter)h.get("Dog")).i++;</p>

  <p>if(o instanceof Pug)</p>

  <p>((Counter)h.get("Pug")).i++;</p>

  <p>if(o instanceof Cat)</p>

  <p>((Counter)h.get("Cat")).i++;</p>

  <p>if(o instanceof Rodent)</p>

  <p>((Counter)h.get("Rodent")).i++;</p>

  <p>if(o instanceof Gerbil)</p>

  <p>((Counter)h.get("Gerbil")).i++;</p>

  <p>if(o instanceof Hamster)</p>

  <p>((Counter)h.get("Hamster")).i++;</p>

  <p>}</p>

  <p>for(int i = 0; i &lt; pets.size(); i++)</p>

  <p>System.out.println(</p>

  <p>pets.elementAt(i).getClass().toString());</p>

  <p>for(int i = 0; i &lt; typenames.length; i++)</p>

  <p>System.out.println(</p>

  <p>typenames[i] + " quantity: " +</p>

  <p>((Counter)h.get(typenames[i])).i);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在Java 1.0中，对instanceof有一个比较小的限制：只可将其与一个已命名的类型比较，不能同Class对象作对比。在上述例子中，大家可能觉得将所有那些instanceof表达式写出来是件很麻烦的事情。实际情况正是这样。但在Java 1.0中，没有办法让这一工作自动进行――不能创建Class的一个Vector，再将其与之比较。大家最终会意识到，如编写了数量众多的instanceof表达式，整个设计都可能出现问题。</p>

  <p>当然，这个例子只是一个构想――最好在每个类型里添加一个static数据成员，然后在构建器中令其增值，以便跟踪计数。编写程序时，大家可能想象自己拥有类的源码控制权，能够自由改动它。但由于实际情况并非总是这样，所以RTTI显得特别方便。</p>

  <p>1. 使用类标记</p>

  <p>PetCount.java示例可用Java 1.1的类标记重写一遍。得到的结果显得更加明确易懂：</p>

  <p>//: PetCount2.java</p>

  <p>// Using Java 1.1 class literals</p>

  <p>package c11.petcount2;</p>

  <p>import java.util.*;</p>

  <p>class Pet {}</p>

  <p>class Dog extends Pet {}</p>

  <p>class Pug extends Dog {}</p>

  <p>class Cat extends Pet {}</p>

  <p>class Rodent extends Pet {}</p>

  <p>class Gerbil extends Rodent {}</p>

  <p>class Hamster extends Rodent {}</p>

  <p>class Counter { int i; }</p>

  <p>public class PetCount2 {</p>

  <p>public static void main(String[] args) {</p>

  <p>Vector pets = new Vector();</p>

  <p>Class[] petTypes = {</p>

  <p>// Class literals work in Java 1.1+ only:</p>

  <p>Pet.class,</p>

  <p>Dog.class,</p>

  <p>Pug.class,</p>

  <p>Cat.class,</p>

  <p>Rodent.class,</p>

  <p>Gerbil.class,</p>

  <p>Hamster.class,</p>

  <p>};</p>

  <p>try {</p>

  <p>for(int i = 0; i &lt; 15; i++) {</p>

  <p>// Offset by one to eliminate Pet.class:</p>

  <p>int rnd = 1 + (int)(</p>

  <p>Math.random() * (petTypes.length - 1));</p>

  <p>pets.addElement(</p>

  <p>petTypes[rnd].newInstance());</p>

  <p>}</p>

  <p>} catch(InstantiationException e) {}</p>

  <p>catch(IllegalAccessException e) {}</p>

  <p>Hashtable h = new Hashtable();</p>

  <p>for(int i = 0; i &lt; petTypes.length; i++)</p>

  <p>h.put(petTypes[i].toString(),</p>

  <p>new Counter());</p>

  <p>for(int i = 0; i &lt; pets.size(); i++) {</p>

  <p>Object o = pets.elementAt(i);</p>

  <p>if(o instanceof Pet)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Pet")).i++;</p>

  <p>if(o instanceof Dog)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Dog")).i++;</p>

  <p>if(o instanceof Pug)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Pug")).i++;</p>

  <p>if(o instanceof Cat)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Cat")).i++;</p>

  <p>if(o instanceof Rodent)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Rodent")).i++;</p>

  <p>if(o instanceof Gerbil)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Gerbil")).i++;</p>

  <p>if(o instanceof Hamster)</p>

  <p>((Counter)h.get(</p>

  <p>"class c11.petcount2.Hamster")).i++;</p>

  <p>}</p>

  <p>for(int i = 0; i &lt; pets.size(); i++)</p>

  <p>System.out.println(</p>

  <p>pets.elementAt(i).getClass().toString());</p>

  <p>Enumeration keys = h.keys();</p>

  <p>while(keys.hasMoreElements()) {</p>

  <p>String nm = (String)keys.nextElement();</p>

  <p>Counter cnt = (Counter)h.get(nm);</p>

  <p>System.out.println(</p>

  <p>nm.substring(nm.lastIndexOf('.') + 1) +</p>

  <p>" quantity: " + cnt.i);</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在这里，typenames（类型名）数组已被删除，改为从Class对象里获取类型名称。注意为此而额外做的工作：例如，类名不是Getbil，而是c11.petcount2.Getbil，其中已包含了包的名字。也要注意系统是能够区分类和接口的。</p>

  <p>也可以看到，petTypes的创建模块不需要用一个try块包围起来，因为它会在编译期得到检查，不会象Class.forName()那样“掷”出任何违例。</p>

  <p>Pet动态创建好以后，可以看到随机数字已得到了限制，位于1和petTypes.length之间，而且不包括零。那是由于零代表的是Pet.class，而且一个普通的Pet对象可能不会有人感兴趣。然而，由于Pet.class是petTypes的一部分，所以所有Pet（宠物）都会算入计数中。</p>

  <p>2. 动态的instanceof</p>

  <p>Java 1.1为Class类添加了isInstance方法。利用它可以动态调用instanceof运算符。而在Java 1.0中，只能静态地调用它（就象前面指出的那样）。因此，所有那些烦人的instanceof语句都可以从PetCount例子中删去了。如下所示：</p>

  <p>//: PetCount3.java</p>

  <p>// Using Java 1.1 isInstance()</p>

  <p>package c11.petcount3;</p>

  <p>import java.util.*;</p>

  <p>class Pet {}</p>

  <p>class Dog extends Pet {}</p>

  <p>class Pug extends Dog {}</p>

  <p>class Cat extends Pet {}</p>

  <p>class Rodent extends Pet {}</p>

  <p>class Gerbil extends Rodent {}</p>

  <p>class Hamster extends Rodent {}</p>

  <p>class Counter { int i; }</p>

  <p>public class PetCount3 {</p>

  <p>public static void main(String[] args) {</p>

  <p>Vector pets = new Vector();</p>

  <p>Class[] petTypes = {</p>

  <p>Pet.class,</p>

  <p>Dog.class,</p>

  <p>Pug.class,</p>

  <p>Cat.class,</p>

  <p>Rodent.class,</p>

  <p>Gerbil.class,</p>

  <p>Hamster.class,</p>

  <p>};</p>

  <p>try {</p>

  <p>for(int i = 0; i &lt; 15; i++) {</p>

  <p>// Offset by one to eliminate Pet.class:</p>

  <p>int rnd = 1 + (int)(</p>

  <p>Math.random() * (petTypes.length - 1));</p>

  <p>pets.addElement(</p>

  <p>petTypes[rnd].newInstance());</p>

  <p>}</p>

  <p>} catch(InstantiationException e) {}</p>

  <p>catch(IllegalAccessException e) {}</p>

  <p>Hashtable h = new Hashtable();</p>

  <p>for(int i = 0; i &lt; petTypes.length; i++)</p>

  <p>h.put(petTypes[i].toString(),</p>

  <p>new Counter());</p>

  <p>for(int i = 0; i &lt; pets.size(); i++) {</p>

  <p>Object o = pets.elementAt(i);</p>

  <p>// Using isInstance to eliminate individual</p>

  <p>// instanceof expressions:</p>

  <p>for (int j = 0; j &lt; petTypes.length; ++j)</p>

  <p>if (petTypes[j].isInstance(o)) {</p>

  <p>String key = petTypes[j].toString();</p>

  <p>((Counter)h.get(key)).i++;</p>

  <p>}</p>

  <p>}</p>

  <p>for(int i = 0; i &lt; pets.size(); i++)</p>

  <p>System.out.println(</p>

  <p>pets.elementAt(i).getClass().toString());</p>

  <p>Enumeration keys = h.keys();</p>

  <p>while(keys.hasMoreElements()) {</p>

  <p>String nm = (String)keys.nextElement();</p>

  <p>Counter cnt = (Counter)h.get(nm);</p>

  <p>System.out.println(</p>

  <p>nm.substring(nm.lastIndexOf('.') + 1) +</p>

  <p>" quantity: " + cnt.i);</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>可以看到，Java 1.1的isInstance()方法已取消了对instanceof表达式的需要。此外，这也意味着一旦要求添加新类型宠物，只需简单地改变petTypes数组即可；毋需改动程序剩余的部分（但在使用instanceof时却是必需的）。</p>

  <p>11.2 RTTI语法</p>

  <p>Java用Class对象实现自己的RTTI功能――即便我们要做的只是象造型那样的一些工作。Class类也提供了其他大量方式，以方便我们使用RTTI。</p>

  <p>首先必须获得指向适当Class对象的的一个句柄。就象前例演示的那样，一个办法是用一个字串以及Class.forName()方法。这是非常方便的，因为不需要那种类型的一个对象来获取Class句柄。然而，对于自己感兴趣的类型，如果已有了它的一个对象，那么为了取得Class句柄，可调用属于Object根类一部分的一个方法：getClass()。它的作用是返回一个特定的Class句柄，用来表示对象的实际类型。Class提供了几个有趣且较为有用的方法，从下例即可看出：</p>

  <p>//: ToyTest.java</p>

  <p>// Testing class Class</p>

  <p>interface HasBatteries {}</p>

  <p>interface Waterproof {}</p>

  <p>interface ShootsThings {}</p>

  <p>class Toy {</p>

  <p>// Comment out the following default</p>

  <p>// constructor to see</p>

  <p>// NoSuchMethodError from (*1*)</p>

  <p>Toy() {}</p>

  <p>Toy(int i) {}</p>

  <p>}</p>

  <p>class FancyToy extends Toy</p>

  <p>implements HasBatteries,</p>

  <p>Waterproof, ShootsThings {</p>

  <p>FancyToy() { super(1); }</p>

  <p>}</p>

  <p>public class ToyTest {</p>

  <p>public static void main(String[] args) {</p>

  <p>Class c = null;</p>

  <p>try {</p>

  <p>c = Class.forName("FancyToy");</p>

  <p>} catch(ClassNotFoundException e) {}</p>

  <p>printInfo(c);</p>

  <p>Class[] faces = c.getInterfaces();</p>

  <p>for(int i = 0; i &lt; faces.length; i++)</p>

  <p>printInfo(faces[i]);</p>

  <p>Class cy = c.getSuperclass();</p>

  <p>Object o = null;</p>

  <p>try {</p>

  <p>// Requires default constructor:</p>

  <p>o = cy.newInstance(); // (*1*)</p>

  <p>} catch(InstantiationException e) {}</p>

  <p>catch(IllegalAccessException e) {}</p>

  <p>printInfo(o.getClass());</p>

  <p>}</p>

  <p>static void printInfo(Class cc) {</p>

  <p>System.out.println(</p>

  <p>"Class name: " + cc.getName() +</p>

  <p>" is interface? [" +</p>

  <p>cc.isInterface() + "]");</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>从中可以看出，class FancyToy相当复杂，因为它从Toy中继承，并实现了HasBatteries，Waterproof以及ShootsThings的接口。在main()中创建了一个Class句柄，并用位于相应try块内的forName()初始化成FancyToy。</p>

  <p>Class.getInterfaces方法会返回Class对象的一个数组，用于表示包含在Class对象内的接口。</p>

  <p>若有一个Class对象，也可以用getSuperclass()查询该对象的直接基础类是什么。当然，这种做会返回一个Class句柄，可用它作进一步的查询。这意味着在运行期的时候，完全有机会调查到对象的完整层次结构。</p>

  <p>若从表面看，Class的newInstance()方法似乎是克隆（clone()）一个对象的另一种手段。但两者是有区别的。利用newInstance()，我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示的那样，当时没有Toy对象，只有cy――即y的Class对象的一个句柄。利用它可以实现“虚拟构建器”。换言之，我们表达：“尽管我不知道你的准确类型是什么，但请你无论如何都正确地创建自己。”在上述例子中，cy只是一个Class句柄，编译期间并不知道进一步的类型信息。一旦新建了一个实例后，可以得到Object句柄。但那个句柄指向一个Toy对象。当然，如果要将除Object能够接收的其他任何消息发出去，首先必须进行一些调查研究，再进行造型。除此以外，用newInstance()创建的类必须有一个默认构建器。没有办法用newInstance()创建拥有非默认构建器的对象，所以在Java 1.0中可能存在一些限制。然而，Java 1.1的“反射”API（下一节讨论）却允许我们动态地使用类里的任何构建器。</p>

  <p>程序中的最后一个方法是printInfo()，它取得一个Class句柄，通过getName()获得它的名字，并用interface()调查它是不是一个接口。</p>

  <p>该程序的输出如下：</p>

  <p>Class name: FancyToy is interface? [false]</p>

  <p>Class name: HasBatteries is interface? [true]</p>

  <p>Class name: Waterproof is interface? [true]</p>

  <p>Class name: ShootsThings is interface? [true]</p>

  <p>Class name: Toy is interface? [false]</p>

  <p>所以利用Class对象，我们几乎能将一个对象的祖宗十八代都调查出来。</p>

  <p>11.3 反射：运行期类信息</p>

  <p>如果不知道一个对象的准确类型，RTTI会帮助我们调查。但却有一个限制：类型必须是在编译期间已知的，否则就不能用RTTI调查它，进而无法展开下一步的工作。换言之，编译器必须明确知道RTTI要处理的所有类。</p>

  <p>从表面看，这似乎并不是一个很大的限制，但假若得到的是一个不在自己程序空间内的对象的句柄，这时又会怎样呢？事实上，对象的类即使在编译期间也不可由我们的程序使用。例如，假设我们从磁盘或者网络获得一系列字节，而且被告知那些字节代表一个类。由于编译器在编译代码时并不知道那个类的情况，所以怎样才能顺利地使用这个类呢？</p>

  <p>在传统的程序设计环境中，出现这种情况的概率或许很小。但当我们转移到一个规模更大的编程世界中，却必须对这个问题加以高度重视。第一个要注意的是基于组件的程序设计。在这种环境下，我们用“快速应用开发”（RAD）模型来构建程序项目。RAD一般是在应用程序构建工具中内建的。这是编制程序的一种可视途径（在屏幕上以窗体的形式出现）。可将代表不同组件的图标拖曳到窗体中。随后，通过设定这些组件的属性或者值，进行正确的配置。设计期间的配置要求任何组件都是可以“例示”的（即可以自由获得它们的实例）。这些组件也要揭示出自己的一部分内容，允许程序员读取和设置各种值。此外，用于控制GUI事件的组件必须揭示出与相应的方法有关的信息，以便RAD环境帮助程序员用自己的代码覆盖这些由事件驱动的方法。“反射”提供了一种特殊的机制，可以侦测可用的方法，并产生方法名。通过Java Beans（第13章将详细介绍），Java 1.1为这种基于组件的程序设计提供了一个基础结构。</p>

  <p>在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调用”（RMI），它允许Java程序（版本1.1以上）使用由多台机器发布或分布的对象。这种对象的分布可能是由多方面的原因引起的：可能要做一件计算密集型的工作，想对它进行分割，让处于空闲状态的其他机器分担部分工作，从而加快处理进度。某些情况下，可能需要将用于控制特定类型任务（比如多层客户／服务器架构中的“运作规则”）的代码放置在一台特殊的机器上，使这台机器成为对那些行动进行描述的一个通用储藏所。而且可以方便地修改这个场所，使其对系统内的所有方面产生影响（这是一种特别有用的设计思路，因为机器是独立存在的，所以能轻易修改软件！）。分布式计算也能更充分地发挥某些专用硬件的作用，它们特别擅长执行一些特定的任务――例如矩阵逆转――但对常规编程来说却显得太夸张或者太昂贵了。</p>

  <p>在Java 1.1中，Class类（本章前面已有详细论述）得到了扩展，可以支持“反射”的概念。针对Field，Method以及Constructor类（每个都实现了Memberinterface――成员接口），它们都新增了一个库：java.lang.reflect。这些类型的对象都是JVM在运行期创建的，用于代表未知类里对应的成员。这样便可用构建器创建新对象，用get()和set()方法读取和修改与Field对象关联的字段，以及用invoke()方法调用与Method对象关联的方法。此外，我们可调用方法getFields()，getMethods()，getConstructors()，分别返回用于表示字段、方法以及构建器的对象数组（在联机文档中，还可找到与Class类有关的更多的资料）。因此，匿名对象的类信息可在运行期被完整的揭露出来，而在编译期间不需要知道任何东西。</p>

  <p>大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道时，JVM只是简单地检查那个对象，并调查它从属于哪个特定的类（就象以前的RTTI那样）。但在这之后，在我们做其他任何事情之前，Class对象必须载入。因此，用于那种特定类型的.class文件必须能由JVM调用（要么在本地机器内，要么可以通过网络取得）。所以RTTI和“反射”之间唯一的区别就是对RTTI来说，编译器会在编译期打开和检查.class文件。换句话说，我们可以用“普通”方式调用一个对象的所有方法；但对“反射”来说，.class文件在编译期间是不可使用的，而是由运行期环境打开和检查。</p>

  <p>11.3.1 一个类方法提取器</p>

  <p>很少需要直接使用反射工具；之所以在语言中提供它们，仅仅是为了支持其他Java特性，比如对象序列化（第10章介绍）、Java Beans以及RMI（本章后面介绍）。但是，我们许多时候仍然需要动态提取与一个类有关的资料。其中特别有用的工具便是一个类方法提取器。正如前面指出的那样，若检视类定义源码或者联机文档，只能看到在那个类定义中被定义或覆盖的方法，基础类那里还有大量资料拿不到。幸运的是，“反射”做到了这一点，可用它写一个简单的工具，令其自动展示整个接口。下面便是具体的程序：</p>

  <p>//: ShowMethods.java</p>

  <p>// Using Java 1.1 reflection to show all the</p>

  <p>// methods of a class, even if the methods are</p>

  <p>// defined in the base class.</p>

  <p>import java.lang.reflect.*;</p>

  <p>public class ShowMethods {</p>

  <p>static final String usage =</p>

  <p>"usage: \n" +</p>

  <p>"ShowMethods qualified.class.name\n" +</p>

  <p>"To show all methods in class or: \n" +</p>

  <p>"ShowMethods qualified.class.name word\n" +</p>

  <p>"To search for methods involving 'word'";</p>

  <p>public static void main(String[] args) {</p>

  <p>if(args.length &lt; 1) {</p>

  <p>System.out.println(usage);</p>

  <p>System.exit(0);</p>

  <p>}</p>

  <p>try {</p>

  <p>Class c = Class.forName(args[0]);</p>

  <p>Method[] m = c.getMethods();</p>

  <p>Constructor[] ctor = c.getConstructors();</p>

  <p>if(args.length == 1) {</p>

  <p>for (int i = 0; i &lt; m.length; i++)</p>

  <p>System.out.println(m[i].toString());</p>

  <p>for (int i = 0; i &lt; ctor.length; i++)</p>

  <p>System.out.println(ctor[i].toString());</p>

  <p>}</p>

  <p>else {</p>

  <p>for (int i = 0; i &lt; m.length; i++)</p>

  <p>if(m[i].toString()</p>

  <p>.indexOf(args[1])!= -1)</p>

  <p>System.out.println(m[i].toString());</p>

  <p>for (int i = 0; i &lt; ctor.length; i++)</p>

  <p>if(ctor[i].toString()</p>

  <p>.indexOf(args[1])!= -1)</p>

  <p>System.out.println(ctor[i].toString());</p>

  <p>}</p>

  <p>} catch (ClassNotFoundException e) {</p>

  <p>System.out.println("No such class: " + e);</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>Class方法getMethods()和getConstructors()可以分别返回Method和Constructor的一个数组。每个类都提供了进一步的方法，可解析出它们所代表的方法的名字、参数以及返回值。但也可以象这样一样只使用toString()，生成一个含有完整方法签名的字串。代码剩余的部分只是用于提取命令行信息，判断特定的签名是否与我们的目标字串相符（使用indexOf()），并打印出结果。</p>

  <p>这里便用到了“反射”技术，因为由Class.forName()产生的结果不能在编译期间获知，所以所有方法签名信息都会在运行期间提取。若研究一下联机文档中关于“反射”（Reflection）的那部分文字，就会发现它已提供了足够多的支持，可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地，这也属于几乎完全不用我们操心的一个步骤――Java自己会利用这种支持，所以程序设计环境能够控制Java Beans――但它无论如何都是非常有趣的。</p>

  <p>一个有趣的试验是运行java ShowMehods ShowMethods。这样做可得到一个列表，其中包括一个public默认构建器，尽管我们在代码中看见并没有定义一个构建器。我们看到的是由编译器自动合成的那一个构建器。如果随之将ShowMethods设为一个非public类（即换成“友好”类），合成的默认构建器便不会在输出结果中出现。合成的默认构建器会自动获得与类一样的访问权限。</p>

  <p>ShowMethods的输出仍然有些“不爽”。例如，下面是通过调用java ShowMethods java.lang.String得到的输出结果的一部分：</p>

  <p>public boolean</p>

  <p>java.lang.String.startsWith(java.lang.String,int)</p>

  <p>public boolean</p>

  <p>java.lang.String.startsWith(java.lang.String)</p>

  <p>public boolean</p>

  <p>java.lang.String.endsWith(java.lang.String)</p>

  <p>若能去掉象java.lang这样的限定词，结果显然会更令人满意。有鉴于此，可引入上一章介绍的StreamTokenizer类，解决这个问题：</p>

  <p>//: ShowMethodsClean.java</p>

  <p>// ShowMethods with the qualifiers stripped</p>

  <p>// to make the results easier to read</p>

  <p>import java.lang.reflect.*;</p>

  <p>import java.io.*;</p>

  <p>public class ShowMethodsClean {</p>

  <p>static final String usage =</p>

  <p>"usage: \n" +</p>

  <p>"ShowMethodsClean qualified.class.name\n" +</p>

  <p>"To show all methods in class or: \n" +</p>

  <p>"ShowMethodsClean qualif.class.name word\n" +</p>

  <p>"To search for methods involving 'word'";</p>

  <p>public static void main(String[] args) {</p>

  <p>if(args.length &lt; 1) {</p>

  <p>System.out.println(usage);</p>

  <p>System.exit(0);</p>

  <p>}</p>

  <p>try {</p>

  <p>Class c = Class.forName(args[0]);</p>

  <p>Method[] m = c.getMethods();</p>

  <p>Constructor[] ctor = c.getConstructors();</p>

  <p>// Convert to an array of cleaned Strings:</p>

  <p>String[] n =</p>

  <p>new String[m.length + ctor.length];</p>

  <p>for(int i = 0; i &lt; m.length; i++) {</p>

  <p>String s = m[i].toString();</p>

  <p>n[i] = StripQualifiers.strip(s);</p>

  <p>}</p>

  <p>for(int i = 0; i &lt; ctor.length; i++) {</p>

  <p>String s = ctor[i].toString();</p>

  <p>n[i + m.length] =</p>

  <p>StripQualifiers.strip(s);</p>

  <p>}</p>

  <p>if(args.length == 1)</p>

  <p>for (int i = 0; i &lt; n.length; i++)</p>

  <p>System.out.println(n[i]);</p>

  <p>else</p>

  <p>for (int i = 0; i &lt; n.length; i++)</p>

  <p>if(n[i].indexOf(args[1])!= -1)</p>

  <p>System.out.println(n[i]);</p>

  <p>} catch (ClassNotFoundException e) {</p>

  <p>System.out.println("No such class: " + e);</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>class StripQualifiers {</p>

  <p>private StreamTokenizer st;</p>

  <p>public StripQualifiers(String qualified) {</p>

  <p>st = new StreamTokenizer(</p>

  <p>new StringReader(qualified));</p>

  <p>st.ordinaryChar(' '); // Keep the spaces</p>

  <p>}</p>

  <p>public String getNext() {</p>

  <p>String s = null;</p>

  <p>try {</p>

  <p>if(st.nextToken() !=</p>

  <p>StreamTokenizer.TT_EOF) {</p>

  <p>switch(st.ttype) {</p>

  <p>case StreamTokenizer.TT_EOL:</p>

  <p>s = null;</p>

  <p>break;</p>

  <p>case StreamTokenizer.TT_NUMBER:</p>

  <p>s = Double.toString(st.nval);</p>

  <p>break;</p>

  <p>case StreamTokenizer.TT_WORD:</p>

  <p>s = new String(st.sval);</p>

  <p>break;</p>

  <p>default: // single character in ttype</p>

  <p>s = String.valueOf((char)st.ttype);</p>

  <p>}</p>

  <p>}</p>

  <p>} catch(IOException e) {</p>

  <p>System.out.println(e);</p>

  <p>}</p>

  <p>return s;</p>

  <p>}</p>

  <p>public static String strip(String qualified) {</p>

  <p>StripQualifiers sq =</p>

  <p>new StripQualifiers(qualified);</p>

  <p>String s = "", si;</p>

  <p>while((si = sq.getNext()) != null) {</p>

  <p>int lastDot = si.lastIndexOf('.');</p>

  <p>if(lastDot != -1)</p>

  <p>si = si.substring(lastDot + 1);</p>

  <p>s += si;</p>

  <p>}</p>

  <p>return s;</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>ShowMethodsClean方法非常接近前一个ShowMethods，只是它取得了Method和Constructor数组，并将它们转换成单个String数组。随后，每个这样的String对象都在StripQualifiers.Strip()里“过”一遍，删除所有方法限定词。正如大家看到的那样，此时用到了StreamTokenizer和String来完成这个工作。</p>

  <p>假如记不得一个类是否有一个特定的方法，而且不想在联机文档里逐步检查类结构，或者不知道那个类是否能对某个对象（如Color对象）做某件事情，该工具便可节省大量编程时间。</p>

  <p>第17章提供了这个程序的一个GUI版本，可在自己写代码的时候运行它，以便快速查找需要的东西。</p>

  <p>11.4 总结</p>

  <p>利用RTTI可根据一个匿名的基础类句柄调查出类型信息。但正是由于这个原因，新手们极易误用它，因为有些时候多形性方法便足够了。对那些以前习惯程序化编程的人来说，极易将他们的程序组织成一系列switch语句。他们可能用RTTI做到这一点，从而在代码开发和维护中损失多形性技术的重要价值。Java的要求是让我们尽可能地采用多形性，只有在极特别的情况下才使用RTTI。</p>

  <p>但为了利用多形性，要求我们拥有对基础类定义的控制权，因为有些时候在程序范围之内，可能发现基础类并未包括我们想要的方法。若基础类来自一个库，或者由别的什么东西控制着，RTTI便是一种很好的解决方案：可继承一个新类型，然后添加自己的额外方法。在代码的其他地方，可以侦测自己的特定类型，并调用那个特殊的方法。这样做不会破坏多形性以及程序的扩展能力，因为新类型的添加不要求查找程序中的switch语句。但在需要新特性的主体中添加新代码时，就必须用RTTI侦测自己特定的类型。</p>

  <p>从某个特定类的利益的角度出发，在基础类里加入一个特性后，可能意味着从那个基础类衍生的其他所有类都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基础类继承，且必须覆盖抽象方法，这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器（Instrument）。假定我们想清洁管弦乐队中所有适当乐器的通气音栓（Spit Valve），此时的一个办法是在基础类Instrument中置入一个ClearSpitValve()方法。但这样做会造成一个误区，因为它暗示着打击乐器和电子乐器中也有音栓。针对这种情况，RTTI提供了一个更合理的解决方案，可将方法置入特定的类中（此时是Wind，即“通气口”）――这样做是可行的。但事实上一种更合理的方案是将prepareInstrument()置入基础类中。初学者刚开始时往往看不到这一点，一般会认定自己必须使用RTTI。</p>

  <p>最后，RTTI有时能解决效率问题。若代码大量运用了多形性，但其中的一个对象在执行效率上很有问题，便可用RTTI找出那个类型，然后写一段适当的代码，改进其效率。</p>

  <p>11.5 练习</p>

  <p>(1) 写一个方法，向它传递一个对象，循环打印出对象层次结构中的所有类。</p>

  <p>(2) 在ToyTest.java中，将Toy的默认构建器标记成注释信息，解释随之发生的事情。</p>

  <p>(3) 新建一种类型的集合，令其使用一个Vector。捕获置入其中的第一个对象的类型，然后从那时起只允许用户插入那种类型的对象。</p>

  <p>(4) 写一个程序，判断一个Char数组属于基本数据类型，还是一个真正的对象。</p>

  <p>(5) 根据本章的说明，实现clearSpitValve()。</p>

  <p>(6) 实现本章介绍的rotate(Shape)方法，令其检查是否已经旋转了一个圆（若已旋转，就不再执行旋转操作）。英文版主页 | 中文版主页 | 详细目录 | 关于译者</p>

  <p>-------------------------------------------------</p>

  <p>TXT书库 www.16txt.com</p>

  <p>-------------------------------------------------第12章 传递和返回对象</p>

  <p>到目前为止，读者应对对象的“传递”有了一个较为深刻的认识，记住实际传递的只是一个句柄。</p>

  <p>在许多程序设计语言中，我们可用语言的“普通”方式到处传递对象，而且大多数时候都不会遇到问题。但有些时候却不得不采取一些非常做法，使得情况突然变得稍微复杂起来（在C++中则是变得非常复杂）。Java亦不例外，我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。</p>

  <p>若读者是从某些特殊的程序设计环境中转移过来的，那么一般都会问到：“Java有指针吗？”有些人认为指针的操作很困难，而且十分危险，所以一厢情愿地认为它没有好处。同时由于Java有如此好的口碑，所以应该很轻易地免除自己以前编程中的麻烦，其中不可能夹带有指针这样的“危险品”。然而准确地说，Java是有指针的！事实上，Java中每个对象（除基本数据类型以外）的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范，不仅编译器对它们有“戒心”，运行期系统也不例外。或者换从另一个角度说，Java有指针，但没有传统指针的麻烦。我曾一度将这种指针叫做“句柄”，但你可以把它想像成“安全指针”。和预备学校为学生提供的安全剪刀类似――除非特别有意，否则不会伤着自己，只不过有时要慢慢来，要习惯一些沉闷的工作。</p>

  <p>12.1 传递句柄</p>

  <p>将句柄传递进入一个方法时，指向的仍然是相同的对象。一个简单的实验可以证明这一点（若执行这个程序时有麻烦，请参考第3章3.1.2小节“赋值”）：</p>

  <p>//: PassHandles.java</p>

  <p>// Passing handles around</p>

  <p>package c12;</p>

  <p>public class PassHandles {</p>

  <p>static void f(PassHandles h) {</p>

  <p>System.out.println("h inside f(): " + h);</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>PassHandles p = new PassHandles();</p>

  <p>System.out.println("p inside main(): " + p);</p>

  <p>f(p);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>toString方法会在打印语句里自动调用，而PassHandles直接从Object继承，没有toString的重新定义。因此，这里会采用toString的Object版本，打印出对象的类，接着是那个对象所在的位置（不是句柄，而是对象的实际存储位置）。输出结果如下：</p>

  <p>p inside main(): PassHandles@1653748</p>

  <p>h inside f() : PassHandles@1653748</p>

  <p>可以看到，无论p还是h引用的都是同一个对象。这比复制一个新的PassHandles对象有效多了，使我们能将一个参数发给一个方法。但这样做也带来了另一个重要的问题。</p>

  <p>12.1.1 别名问题</p>

  <p>“别名”意味着多个句柄都试图指向同一个对象，就象前面的例子展示的那样。若有人向那个对象里写入一点什么东西，就会产生别名问题。若其他句柄的所有者不希望那个对象改变，恐怕就要失望了。这可用下面这个简单的例子说明：</p>

  <p>//: Alias1.java</p>

  <p>// Aliasing two handles to one object</p>

  <p>public class Alias1 {</p>

  <p>int i;</p>

  <p>Alias1(int ii) { i = ii; }</p>

  <p>public static void main(String[] args) {</p>

  <p>Alias1 x = new Alias1(7);</p>

  <p>Alias1 y = x; // Assign the handle</p>

  <p>System.out.println("x: " + x.i);</p>

  <p>System.out.println("y: " + y.i);</p>

  <p>System.out.println("Incrementing x");</p>

  <p>x.i++;</p>

  <p>System.out.println("x: " + x.i);</p>

  <p>System.out.println("y: " + y.i);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>对下面这行：</p>

  <p>Alias1 y = x; // Assign the handle</p>

  <p>它会新建一个Alias1句柄，但不是把它分配给由new创建的一个新鲜对象，而是分配给一个现有的句柄。所以句柄x的内容――即对象x指向的地址――被分配给y，所以无论x还是y都与相同的对象连接起来。这样一来，一旦x的i在下述语句中增值：</p>

  <p>x.i++;</p>

  <p>y的i值也必然受到影响。从最终的输出就可以看出：</p>

  <p>x: 7</p>

  <p>y: 7</p>

  <p>Incrementing x</p>

  <p>x: 8</p>

  <p>y: 8</p>

  <p>此时最直接的一个解决办法就是干脆不这样做：不要有意将多个句柄指向同一个作用域内的同一个对象。这样做可使代码更易理解和调试。然而，一旦准备将句柄作为一个自变量或参数传递――这是Java设想的正常方法――别名问题就会自动出现，因为创建的本地句柄可能修改“外部对象”（在方法作用域之外创建的对象）。下面是一个例子：</p>

  <p>//: Alias2.java</p>

  <p>// Method calls implicitly alias their</p>

  <p>// arguments.</p>

  <p>public class Alias2 {</p>

  <p>int i;</p>

  <p>Alias2(int ii) { i = ii; }</p>

  <p>static void f(Alias2 handle) {</p>

  <p>handle.i++;</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>Alias2 x = new Alias2(7);</p>

  <p>System.out.println("x: " + x.i);</p>

  <p>System.out.println("Calling f(x)");</p>

  <p>f(x);</p>

  <p>System.out.println("x: " + x.i);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>输出如下：</p>

  <p>x: 7</p>

  <p>Calling f(x)</p>

  <p>x: 8</p>

  <p>方法改变了自己的参数――外部对象。一旦遇到这种情况，必须判断它是否合理，用户是否愿意这样，以及是不是会造成问题。</p>

  <p>通常，我们调用一个方法是为了产生返回值，或者用它改变为其调用方法的那个对象的状态（方法其实就是我们向那个对象“发一条消息”的方式）。很少需要调用一个方法来处理它的参数；这叫作利用方法的“副作用”（Side Effect）。所以倘若创建一个会修改自己参数的方法，必须向用户明确地指出这一情况，并警告使用那个方法可能会有的后果以及它的潜在威胁。由于存在这些混淆和缺陷，所以应该尽量避免改变参数。</p>

  <p>若需在一个方法调用期间修改一个参数，且不打算修改外部参数，就应在自己的方法内部制作一个副本，从而保护那个参数。本章的大多数内容都是围绕这个问题展开的。</p>

  <p>12.2 制作本地副本</p>

  <p>稍微总结一下：Java中的所有自变量或参数传递都是通过传递句柄进行的。也就是说，当我们传递“一个对象”时，实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修改，便相当于修改外部对象。此外：</p>

  <p>■参数传递过程中会自动产生别名问题</p>

  <p>■不存在本地对象，只有本地句柄</p>

  <p>■句柄有自己的作用域，而对象没有</p>

  <p>■对象的“存在时间”在Java里不是个问题</p>

  <p>■没有语言上的支持（如常量）可防止对象被修改（以避免别名的副作用）</p>

  <p>若只是从对象中读取信息，而不修改它，传递句柄便是自变量传递中最有效的一种形式。这种做非常恰当；默认的方法一般也是最有效的方法。然而，有时仍需将对象当作“本地的”对待，使我们作出的改变只影响一个本地副本，不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本地副本（注释①）。尽管Java不具备这种能力，但允许我们达到同样的效果。</p>

  <p>①：在C语言中，通常控制的是少量数据位，默认操作是按值传递。C++也必须遵照这一形式，但按值传递对象并非肯定是一种有效的方式。此外，在C++中用于支持按值传递的代码也较难编写，是件让人头痛的事情。</p>

  <p>12.2.1 按值传递</p>

  <p>首先要解决术语的问题，最适合“按值传递”的看起来是自变量。“按值传递”以及它的含义取决于如何理解程序的运行方式。最常见的意思是获得要传递的任何东西的一个本地副本，但这里真正的问题是如何看待自己准备传递的东西。对于“按值传递”的含义，目前存在两种存在明显区别的见解：</p>

  <p>(1) Java按值传递任何东西。若将基本数据类型传递进入一个方法，会明确得到基本数据类型的一个副本。但若将一个句柄传递进入方法，得到的是句柄的副本。所以人们认为“一切”都按值传递。当然，这种说法也有一个前提：句柄肯定也会被传递。但Java的设计方案似乎有些超前，允许我们忽略（大多数时候）自己处理的是一个句柄。也就是说，它允许我们将句柄假想成“对象”，因为在发出方法调用时，系统会自动照管两者间的差异。</p>

  <p>(2) Java主要按值传递（无自变量），但对象却是按引用传递的。得到这个结论的前提是句柄只是对象的一个“别名”，所以不考虑传递句柄的问题，而是直接指出“我准备传递对象”。由于将其传递进入一个方法时没有获得对象的一个本地副本，所以对象显然不是按值传递的。Sun公司似乎在某种程度上支持这一见解，因为它“保留但未实现”的关键字之一便是byvalue（按值）。但没人知道那个关键字什么时候可以发挥作用。</p>

  <p>尽管存在两种不同的见解，但其间的分歧归根到底是由于对“句柄”的不同解释造成的。我打算在本书剩下的部分里回避这个问题。大家不久就会知道，这个问题争论下去其实是没有意义的――最重要的是理解一个句柄的传递会使调用者的对象发生意外的改变。</p>

  <p>12.2.2 克隆对象</p>

  <p>若需修改一个对象，同时不想改变调用者的对象，就要制作该对象的一个本地副本。这也是本地副本最常见的一种用途。若决定制作一个本地副本，只需简单地使用clone()方法即可。Clone是“克隆”的意思，即制作完全一模一样的副本。这个方法在基础类Object中定义成“protected”（受保护）模式。但在希望克隆的任何衍生类中，必须将其覆盖为“public”模式。例如，标准库类Vector覆盖了clone()，所以能为Vector调用clone()，如下所示：</p>

  <p>//: Cloning.java</p>

  <p>// The clone() operation works for only a few</p>

  <p>// items in the standard Java library.</p>

  <p>import java.util.*;</p>

  <p>class Int {</p>

  <p>private int i;</p>

  <p>public Int(int ii) { i = ii; }</p>

  <p>public void increment() { i++; }</p>

  <p>public String toString() {</p>

  <p>return Integer.toString(i);</p>

  <p>}</p>

  <p>}</p>

  <p>public class Cloning {</p>

  <p>public static void main(String[] args) {</p>

  <p>Vector v = new Vector();</p>

  <p>for(int i = 0; i &lt; 10; i++ )</p>

  <p>v.addElement(new Int(i));</p>

  <p>System.out.println("v: " + v);</p>

  <p>Vector v2 = (Vector)v.clone();</p>

  <p>// Increment all v2's elements:</p>

  <p>for(Enumeration e = v2.elements();</p>

  <p>e.hasMoreElements(); )</p>

  <p>((Int)e.nextElement()).increment();</p>

  <p>// See if it changed v's elements:</p>

  <p>System.out.println("v: " + v);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>clone()方法产生了一个Object，后者必须立即重新造型为正确类型。这个例子指出Vector的clone()方法不能自动尝试克隆Vector内包含的每个对象――由于别名问题，老的Vector和克隆的Vector都包含了相同的对象。我们通常把这种情况叫作“简单复制”或者“浅层复制”，因为它只复制了一个对象的“表面”部分。实际对象除包含这个“表面”以外，还包括句柄指向的所有对象，以及那些对象又指向的其他所有对象，由此类推。这便是“对象网”或“对象关系网”的由来。若能复制下所有这张网，便叫作“全面复制”或者“深层复制”。</p>

  <p>在输出中可看到浅层复制的结果，注意对v2采取的行动也会影响到v：</p>

  <p>v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</p>

  <p>v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]</p>

  <p>一般来说，由于不敢保证Vector里包含的对象是“可以克隆”（注释②）的，所以最好不要试图克隆那些对象。</p>

  <p>②：“可以克隆”用英语讲是cloneable，请留意Java库中专门保留了这样的一个关键字。</p>

  <p>12.2.3 使类具有克隆能力</p>

  <p>尽管克隆方法是在所有类最基本的Object中定义的，但克隆仍然不会在每个类里自动进行。这似乎有些不可思议，因为基础类方法在衍生类里是肯定能用的。但Java确实有点儿反其道而行之；如果想在一个类里使用克隆方法，唯一的办法就是专门添加一些代码，以便保证克隆的正常进行。</p>

  <p>1. 使用protected时的技巧</p>

  <p>为避免我们创建的每个类都默认具有克隆能力，clone()方法在基础类Object里得到了“保留”（设为protected）。这样造成的后果就是：对那些简单地使用一下这个类的客户程序员来说，他们不会默认地拥有这个方法；其次，我们不能利用指向基础类的一个句柄来调用clone()（尽管那样做在某些情况下特别有用，比如用多形性的方式克隆一系列对象）。在编译期的时候，这实际是通知我们对象不可克隆的一种方式――而且最奇怪的是，Java库中的大多数类都不能克隆。因此，假如我们执行下述代码：</p>

  <p>Integer x = new Integer(l);</p>

  <p>x = x.clone();</p>

  <p>那么在编译期，就有一条讨厌的错误消息弹出，告诉我们不可访问clone()――因为Integer并没有覆盖它，而且它对protected版本来说是默认的）。</p>

  <p>但是，假若我们是在一个从Object衍生出来的类中（所有类都是从Object衍生的），就有权调用Object.clone()，因为它是“protected”，而且我们在一个继承器中。基础类clone()提供了一个有用的功能――它进行的是对衍生类对象的真正“按位”复制，所以相当于标准的克隆行动。然而，我们随后需要将自己的克隆操作设为public，否则无法访问。总之，克隆时要注意的两个关键问题是：几乎肯定要调用super.clone()，以及注意将克隆设为public。</p>

  <p>有时还想在更深层的衍生类中覆盖clone()，否则就直接使用我们的clone()（现在已成为public），而那并不一定是我们所希望的（然而，由于Object.clone()已制作了实际对象的一个副本，所以也有可能允许这种情况）。protected的技巧在这里只能用一次：首次从一个不具备克隆能力的类继承，而且想使一个类变成“能够克隆”。而在从我们的类继承的任何场合，clone()方法都是可以使用的，因为Java不可能在衍生之后反而缩小方法的访问范围。换言之，一旦对象变得可以克隆，从它衍生的任何东西都是能够克隆的，除非使用特殊的机制（后面讨论）令其“关闭”克隆能力。</p>

  <p>2. 实现Cloneable接口</p>

  <p>为使一个对象的克隆能力功成圆满，还需要做另一件事情：实现Cloneable接口。这个接口使人稍觉奇怪，因为它是空的！</p>

  <p>interface Cloneable {}</p>

  <p>之所以要实现这个空接口，显然不是因为我们准备上溯造型成一个Cloneable，以及调用它的某个方法。有些人认为在这里使用接口属于一种“欺骗”行为，因为它使用的特性打的是别的主意，而非原来的意思。Cloneable interface的实现扮演了一个标记的角色，封装到类的类型中。</p>

  <p>两方面的原因促成了Cloneable interface的存在。首先，可能有一个上溯造型句柄指向一个基础类型，而且不知道它是否真的能克隆那个对象。在这种情况下，可用instanceof关键字（第11章有介绍）调查句柄是否确实同一个能克隆的对象连接：</p>

  <p>if(myHandle instanceof Cloneable) // ...</p>

  <p>第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以Object.clone()会验证一个类是否真的是实现了Cloneable接口。若答案是否定的，则“掷”出一个CloneNotSupportedException违例。所以在一般情况下，我们必须将“implement Cloneable”作为对克隆能力提供支持的一部分。</p>

  <p>12.2.4 成功的克隆</p>

  <p>理解了实现clone()方法背后的所有细节后，便可创建出能方便复制的类，以便提供了一个本地副本：</p>

  <p>//: LocalCopy.java</p>

  <p>// Creating local copies with clone()</p>

  <p>import java.util.*;</p>

  <p>class MyObject implements Cloneable {</p>

  <p>int i;</p>

  <p>MyObject(int ii) { i = ii; }</p>

  <p>public Object clone() {</p>

  <p>Object o = null;</p>

  <p>try {</p>

  <p>o = super.clone();</p>

  <p>} catch (CloneNotSupportedException e) {</p>

  <p>System.out.println("MyObject can't clone");</p>

  <p>}</p>

  <p>return o;</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>return Integer.toString(i);</p>

  <p>}</p>

  <p>}</p>

  <p>public class LocalCopy {</p>

  <p>static MyObject g(MyObject v) {</p>

  <p>// Passing a handle, modifies outside object:</p>

  <p>v.i++;</p>

  <p>return v;</p>

  <p>}</p>

  <p>static MyObject f(MyObject v) {</p>

  <p>v = (MyObject)v.clone(); // Local copy</p>

  <p>v.i++;</p>

  <p>return v;</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>MyObject a = new MyObject(11);</p>

  <p>MyObject b = g(a);</p>

  <p>// Testing handle equivalence,</p>

  <p>// not object equivalence:</p>

  <p>if(a == b)</p>

  <p>System.out.println("a == b");</p>

  <p>else</p>

  <p>System.out.println("a != b");</p>

  <p>System.out.println("a = " + a);</p>

  <p>System.out.println("b = " + b);</p>

  <p>MyObject c = new MyObject(47);</p>

  <p>MyObject d = f(c);</p>

  <p>if(c == d)</p>

  <p>System.out.println("c == d");</p>

  <p>else</p>

  <p>System.out.println("c != d");</p>

  <p>System.out.println("c = " + c);</p>

  <p>System.out.println("d = " + d);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>不管怎样，clone()必须能够访问，所以必须将其设为public（公共的）。其次，作为clone()的初期行动，应调用clone()的基础类版本。这里调用的clone()是Object内部预先定义好的。之所以能调用它，是由于它具有protected（受到保护的）属性，所以能在衍生的类里访问。</p>

  <p>Object.clone()会检查原先的对象有多大，再为新对象腾出足够多的内存，将所有二进制位从原来的对象复制到新对象。这叫作“按位复制”，而且按一般的想法，这个工作应该是由clone()方法来做的。但在Object.clone()正式开始操作前，首先会检查一个类是否Cloneable，即是否具有克隆能力――换言之，它是否实现了Cloneable接口。若未实现，Object.clone()就掷出一个CloneNotSupportedException违例，指出我们不能克隆它。因此，我们最好用一个try-catch块将对super.clone()的调用代码包围（或封装）起来，试图捕获一个应当永不出现的违例（因为这里确实已实现了Cloneable接口）。</p>

  <p>在LocalCopy中，两个方法g()和f()揭示出两种参数传递方法间的差异。其中，g()演示的是按引用传递，它会修改外部对象，并返回对那个外部对象的一个引用。而f()是对自变量进行克隆，所以将其分离出来，并让原来的对象保持独立。随后，它继续做它希望的事情。甚至能返回指向这个新对象的一个句柄，而且不会对原来的对象产生任何副作用。注意下面这个多少有些古怪的语句：</p>

  <p>v = (MyObject)v.clone();</p>

  <p>它的作用正是创建一个本地副本。为避免被这样的一个语句搞混淆，记住这种相当奇怪的编码形式在Java中是完全允许的，因为有一个名字的所有东西实际都是一个句柄。所以句柄v用于克隆一个它所指向的副本，而且最终返回指向基础类型Object的一个句柄（因为它在Object.clone()中是那样被定义的），随后必须将其造型为正确的类型。</p>

  <p>在main()中，两种不同参数传递方式的区别在于它们分别测试了一个不同的方法。输出结果如下：</p>

  <p>a == b</p>

  <p>a = 12</p>

  <p>b = 12</p>

  <p>c != d</p>

  <p>c = 47</p>

  <p>d = 48</p>

  <p>大家要记住这样一个事实：Java对“是否等价”的测试并不对所比较对象的内部进行检查，从而核实它们的值是否相同。==和!=运算符只是简单地对比句柄的内容。若句柄内的地址相同，就认为句柄指向同样的对象，所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题，句柄是否指向同一个对象？”</p>

  <p>12.2.5 Object.clone()的效果</p>

  <p>调用Object.clone()时，实际发生的是什么事情呢？当我们在自己的类里覆盖clone()时，什么东西对于super.clone()来说是最关键的呢？根类中的clone()方法负责建立正确的存储容量，并通过“按位复制”将二进制位从原始对象中复制到新对象的存储空间。也就是说，它并不只是预留存储空间以及复制一个对象――实际需要调查出欲复制之对象的准确大小，然后复制那个对象。由于所有这些工作都是在由根类定义之clone()方法的内部代码中进行的（根类并不知道要从自己这里继承出去什么），所以大家或许已经猜到，这个过程需要用RTTI判断欲克隆的对象的实际大小。采取这种方式，clone()方法便可建立起正确数量的存储空间，并对那个类型进行正确的按位复制。</p>

  <p>不管我们要做什么，克隆过程的第一个部分通常都应该是调用super.clone()。通过进行一次准确的复制，这样做可为后续的克隆进程建立起一个良好的基础。随后，可采取另一些必要的操作，以完成最终的克隆。</p>

  <p>为确切了解其他操作是什么，首先要正确理解Object.clone()为我们带来了什么。特别地，它会自动克隆所有句柄指向的目标吗？下面这个例子可完成这种形式的检测：</p>

  <p>//: Snake.java</p>

  <p>// Tests cloning to see if destination of</p>

  <p>// handles are also cloned.</p>

  <p>public class Snake implements Cloneable {</p>

  <p>private Snake next;</p>

  <p>private char c;</p>

  <p>// Value of i == number of segments</p>

  <p>Snake(int i, char x) {</p>

  <p>c = x;</p>

  <p>if(--i &gt; 0)</p>

  <p>next = new Snake(i, (char)(x + 1));</p>

  <p>}</p>

  <p>void increment() {</p>

  <p>c++;</p>

  <p>if(next != null)</p>

  <p>next.increment();</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>String s = ":" + c;</p>

  <p>if(next != null)</p>

  <p>s += next.toString();</p>

  <p>return s;</p>

  <p>}</p>

  <p>public Object clone() {</p>

  <p>Object o = null;</p>

  <p>try {</p>

  <p>o = super.clone();</p>

  <p>} catch (CloneNotSupportedException e) {}</p>

  <p>return o;</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>Snake s = new Snake(5, 'a');</p>

  <p>System.out.println("s = &amp;</p>

  <div class="mbppagebreak"></div>
</body>
</html>
